package tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;

import org.junit.Before;
import org.junit.Test;

import peerToPeerFacebook.proj.CallPending;
import peerToPeerFacebook.proj.CallPendingImpl;
import peerToPeerFacebook.proj.IResultCallback;
import peerToPeerFacebook.proj.RPCClient;
import peerToPeerFacebook.proj.RPCException;
import peerToPeerFacebook.proj.RPCPacketTooBig;
import peerToPeerFacebook.proj.RPCServer;
import peerToPeerFacebook.proj.RPCTimeout;
import peerToPeerFacebook.proj.Status;

public class RPCTest
{
	protected class ServerCrash extends RuntimeException {
		private static final long serialVersionUID = 7527872624829405866L;
		public ServerCrash() { };
	}
		
	protected class Counter {
		public int counter = 0;
		public int invokeCount = 0;
		public int clientID = 0;
		
		public int increment(int clientID, int count)
		{
			counter += count;
			invokeCount++;
			this.clientID = clientID;
			return counter;
		}
		
		public void crash(int clientID, int count)
		{
			increment(clientID, count);
			throw new ServerCrash();
		}

		public void exception(int clientID, int count) throws Exception
		{
			increment(clientID, count);
			throw new Exception("Pretending that the call failed");
		}
		
		public byte[] big(int clientID, int count)
		{
			increment(clientID, count);
			return new byte[count];
		}
		
		public CallPending callPending(int clientID, int count)
		{
		    increment(clientID, count);
		    return cp;
		}
	}
	
	protected class Result implements IResultCallback {
		public Status status = null;
		public Serializable result = null;
		public int invokeCount = 0;
		
		@Override
		public void resultCallback(Status status, Serializable result)
		{
		    assertNotNull("Status should never be null", status);
			this.status = status;
			this.result = result;
			
			invokeCount++;
		}
		
	}

	protected TestNode cNode = null;
	protected TestNode sNode = null;
	
	protected RPCClient client = null;

	protected Counter counter = null;
	protected Result result = null;
	protected CallPendingImpl cp = null;
	
	@Before
	public void setUp() throws Exception
	{
		counter = new Counter();
		result = new Result();
		cp = new CallPendingImpl();
		
		cNode = new TestNode();
		sNode = new TestNode();
		
		client = new RPCClient(cNode, sNode.getAddr());
		
		cNode.setTargets(sNode, client);
		sNode.setTargets(cNode, new RPCServer(sNode, counter));
	}

	@Test
	public void testSimple() throws RPCException
	{
		client.invoke("increment", new Serializable[] { 3 }, result);
		
		// deliver the packet to server
		assertEquals("Initial call message to the server", 1, sNode.pendingMessages.size());
		
		// make sure that no funky thing is going on
		assertEquals("Counter value before message delivery", 0, counter.counter);
		assertEquals("Counter invocation count before message delivery", 0, counter.invokeCount);
		assertEquals("Result status before invocation", null, result.status);
		assertEquals("Result invocation count before invocation", 0, result.invokeCount);
		
		// deliver the packet to client
		sNode.deliverPacket();
		assertEquals(1, cNode.pendingMessages.size());
		cNode.deliverPacket();
		
		// check the result
		assertEquals("Result status after invocation", Status.Success, result.status);
		assertEquals("Result value after invocation", 3, result.result);
		assertEquals("Invocation count", 1, result.invokeCount);
		assertEquals("Result on server", 3, counter.counter);
		assertEquals("Invocation count on server", 1, counter.invokeCount);
		assertEquals("ClientID", cNode.getAddr(), counter.clientID);
	}
	
	@Test
	public void testDroppedPacket() throws RPCException, IllegalAccessException, InvocationTargetException
	{
		client.invoke("increment", new Serializable[] { 4 }, result);
		
		// let's drop the packet, order is not really relevant
		cNode.timeout();
		cNode.timeout();
		sNode.dropPacket();
		sNode.dropPacket();
		
		// Send the packets in round drip. There is no point in testing what happens
		// if return packet is lost - this is covered in part by this test case (that timeout is running) and
		// by duplicate invocation
		sNode.deliverPacket();
		cNode.deliverPacket();
		
		// Time to check what happened
		assertEquals("Invocation result status", Status.Success, result.status);
		assertEquals("Result returned", 4, result.result);
		assertEquals("Invocation count on server", 1, result.invokeCount);
		assertEquals("Counter value on the server", 4, counter.counter);
		assertEquals("Invocation count on server", 1, counter.invokeCount);	
	}
	
	@Test
	public void testDuplicatePacket() throws RPCException, IllegalAccessException, InvocationTargetException
	{
		client.invoke("increment", new Serializable[] { 5 }, result);
		
		// Let's generate a bunch of packets, however, we do not drop any of them
		cNode.timeout();
		cNode.timeout();
		
		// Send the packets in round drip. There is no point in testing what happens
		// if return packet is lost - this is covered in part by this test case (that timeout is running) and
		// by duplicate invocation
		sNode.deliverAllPackets();
		
		// Value of messages 1 - initial invocation, +2 - timeouts = 3
		assertEquals("Number of return value packets in servers buffer", 3, cNode.pendingMessages.size());
		cNode.deliverAllPackets();
		
		// Time to check what happened
		assertEquals("Result invocation status", Status.Success, result.status);
		assertEquals("Result returned", 5, result.result);
		assertEquals("Invocation count of result", 1, result.invokeCount);
		assertEquals("Result on server", 5, counter.counter);
		assertEquals("Invocation count on server", 1, counter.invokeCount);			
	}
	
	@Test
	public void testServerCrashBeforeInvoke() throws IllegalAccessException, InvocationTargetException, RPCException
	{
		client.invoke("increment", new Serializable[] { 6 }, result);
		
		// Let server process callback
		sNode.deliverAllPackets();
		
		// Need to drop all outgoing packets from the server
		cNode.dropAllpackets();
		
		// bye bye server		
		sNode.setTargets(cNode, new RPCServer(sNode, counter));
		
		// At this point server should have invoked the function, but client should not have invoked callback
		assertEquals("Server function should have been invoked", 1, counter.invokeCount);
		assertEquals("Callback on client should be invoked", 0, result.invokeCount);
		
		// Trigger second round of messages
		cNode.timeout();
		sNode.deliverAllPackets();
		cNode.deliverAllPackets();
		
		// We should still have only one invocation on server and one invocation on client.
		assertEquals("Server function should have been invoked", 1, counter.invokeCount);
		assertEquals("Callback on client should be invoked", 1, result.invokeCount);		
	}

	@Test
	public void testMethodCrash() throws IllegalAccessException, InvocationTargetException, RPCException
	{
		client.invoke("crash", new Serializable[] { 7 }, result);
		
		// so this call will crash
		boolean crashed = false;
		try {
			sNode.deliverAllPackets();
		} catch (ServerCrash ex){
			crashed = true;
		}
		
		assertTrue("Server should have crashed", crashed);
		assertEquals("Server should not have generated any messages", 0, cNode.pendingMessages.size());

		// And clean up server mess
		cNode.dropAllpackets();
		sNode.setTargets(cNode, new RPCServer(sNode, counter));
		
		// Trigger second round of messages
		cNode.timeout();
		sNode.deliverAllPackets();
		cNode.deliverAllPackets();
		
		// In this case function would never have executed. Because we crashed after we updated counters, we would see
		// new values. However, the function should be invoked only once
		assertEquals("Server function should have been invoked", 1, counter.invokeCount);
		assertEquals("Callback on client should be invoked", 1, result.invokeCount);
		assertEquals("Server function should have updated its state", 7, counter.counter);
		assertEquals("Server should not have executed function", Status.AtMostOnce, result.status);
	}

	@Test
	public void testClientCrashAfterReceive() throws RPCException
	{	
		client.invoke("increment", new Serializable[] { 8 }, result);
		
		// deliver messages so that the server would get more confused. If the messages are not delivered, test case
		// is not interesting - server never heard anything from client.
		sNode.deliverAllPackets();
		
		// bye bye client
		client = new RPCClient(cNode, sNode.getAddr());	
		cNode.setTargets(sNode, client);
		
		// Send garbage messages to client, make sure it is not confused
		cNode.deliverAllPackets();
	
		// Check that the function was invoked, but not callback
		assertEquals("Server function should be invoked once", 1, counter.invokeCount);
		assertEquals("Server function result", 8, counter.counter);
		assertEquals("Callback should not be invoked", 0, result.invokeCount);
		
		// Make next call to ensure that RPC is usable
		client.invoke("increment", new Serializable[] { 9 }, result);
		sNode.deliverAllPackets();
		cNode.deliverAllPackets();
		
		// checks for functions
		assertEquals("Server function should be invoked", 2, counter.invokeCount);
		assertEquals("Server function result", 8 + 9, counter.counter);
		assertEquals("Callback should be invoked once", 1, result.invokeCount);
		assertEquals("Result returned from function", 8 + 9, result.result);
		assertEquals("Status of invocation", Status.Success, result.status);
	}
	
	@Test
	public void testClientCrashBeforeReceive() throws RPCException
	{	
		client.invoke("increment", new Serializable[] { 10 }, result);
		
		// deliver messages so that the server would get more confused. If the messages are not delivered, test case
		// is not interesting - server never heard anything from client.
		sNode.deliverAllPackets();
		
		// bye bye client
		client = new RPCClient(cNode, sNode.getAddr());	
		cNode.setTargets(sNode, client);
		
		// client never heard from server, but the server DID hear from the client
		cNode.dropAllpackets();
		
		// Check that the function was invoked, but not callback
		assertEquals("Server function should be invoked once", 1, counter.invokeCount);
		assertEquals("Server function result", 10, counter.counter);
		assertEquals("Callback should not be invoked", 0, result.invokeCount);
		
		// Make next call to ensure that RPC is usable
		client.invoke("increment", new Serializable[] { 11 }, result);
		sNode.deliverAllPackets();
		cNode.deliverAllPackets();
		
		// checks for functions
		assertEquals("Server function should be invoked", 2, counter.invokeCount);
		assertEquals("Server function result", 10 + 11, counter.counter);
		assertEquals("Callback should not invoked", 1, result.invokeCount);
		assertEquals("Result returned from function", 10 + 11, result.result);
		assertEquals("Status of invocation", Status.Success, result.status);
	}
	
	@Test
	public void testExceptionHandling() throws RPCException
	{
		client.invoke("exception", new Serializable[] { 11 }, result);
		sNode.deliverAllPackets();
		cNode.deliverAllPackets();
		
		// Make sure that the call went in fine
		assertEquals("Server function should have been invoked", 1, counter.invokeCount);
		assertEquals("Callback on client should be invoked", 1, result.invokeCount);
		assertEquals("Server function should have updated its state", 11, counter.counter);
		assertEquals("Server should not have executed function", Status.Exception, result.status);
		assertTrue("Result should be exception", result.result instanceof Exception);
	}
	
	@Test
	public void testClientSendPacketTooBig()
	{
		// Send the call with the big stuff
		final int size = 2000;
		byte big[] = new byte[2000];
		cNode.maxPacketSize = size - 1;
		
		client.invoke("increment", new Serializable[] { big }, result);
		
		// check that we were pinged with error
		assertEquals("Call should have failed with exception", Status.Exception, result.status);
		assertTrue("We should have received information that packet is too big", result.result instanceof RPCPacketTooBig);
		assertEquals("Server function should not have been invoked", 0, counter.invokeCount);
	}
	
	@Test
	public void testServerSendPacketTooBig()
	{
		// Send the call with the big stuff. However, we need to make sure that our error packet does fit in there
		final int size = 2000;
		sNode.maxPacketSize = size - 1;
		
		// make call
		client.invoke("big", new Serializable[] { size }, result);
		sNode.deliverAllPackets();
		cNode.deliverAllPackets();
		
		// check that we were pinged with error
		assertEquals("Call should have failed with exception", Status.Exception, result.status);
		assertTrue("We should have received information that packet is too big", result.result instanceof RPCPacketTooBig);
		assertEquals("Server function should have been invoked", 1, counter.invokeCount);
	}
	
	@Test
	public void testClientCallbackWithMultipleCalls() throws IllegalAccessException, InvocationTargetException
	{
		client.invoke("exception", new Serializable[] { 12 }, result);
		sNode.deliverAllPackets();
		cNode.deliverAllPackets();

		// callback was not called yet. Let's schedule next call and make sure we have only one packet
		client.invoke("exception", new Serializable[] { 12 }, result);
		
		// We should have two callbacks scheduled
		assertEquals("Two callbacks - one old and one new", 2, cNode.callbacks.size());
		
		// invoke them and make sure one of them is no-op as it was scheduled for old call
		cNode.timeout();
		assertEquals(2, sNode.pendingMessages.size());
	}
	
	@Test
	public void testRPCTimeout() throws IllegalAccessException, InvocationTargetException
	{		
		client.invoke("increment", new Serializable[] { 13 }, result);
		
		// do not deliver packets, just time out multiple times
		for (int i = 0; i < RPCTimeout.timeOuts; i++)
			cNode.timeout();
		
		// check the result
		assertEquals("RPCClient should have failed with timeout error", Status.Exception, result.status);
		assertTrue("Result should be RPCTimeout exception", result.result instanceof RPCTimeout);
	}
	
	@Test
	public void testCallPending() throws IllegalAccessException, InvocationTargetException
	{
	    client.invoke("callPending", new Serializable[] { 14 }, result);
	    sNode.deliverAllPackets();
	    cNode.deliverAllPackets();
	    
	    // make sure result is not triggered as it is not time, but the call should have happened
	    assertEquals("Result should not be triggered", null, result.status);
	    assertEquals("Function should have been called", 1, counter.invokeCount);
	    
	    // Make sure that we do not decrease timeouts
        for (int i = 0; i < RPCTimeout.timeOuts + 1; i++)
        {
            cNode.timeout();
            sNode.deliverAllPackets();
            cNode.deliverAllPackets();
            
            // Again make sure that call is not triggered
            assertEquals("Result should not be triggered", null, result.status);
            assertEquals("Function should have been called", 1, counter.invokeCount);
        }

        // Now return the result
        cp.setResult(14);
        
        cNode.timeout();
        sNode.deliverAllPackets();
        cNode.deliverAllPackets();

        // This time we should have the result
        assertEquals("Result should be triggered", Status.Success, result.status);
        assertEquals("Result should be triggered", (Integer)14, (Integer)result.result);
        assertEquals("Function should have been called", 1, counter.invokeCount);
	}

    @Test
    public void testCallPendingCrash() throws IllegalAccessException, InvocationTargetException
    {
        client.invoke("callPending", new Serializable[] { 15 }, result);
        sNode.deliverAllPackets();
        cNode.deliverAllPackets();
        
        // make sure result is not triggered as it is not time, but the call should have happened
        assertEquals("Result should not be triggered", null, result.status);
        assertEquals("Result should NOT be triggered", 0, result.invokeCount);
        assertEquals("Function should have been called", 1, counter.invokeCount);
        
        // Crash and restart the server 
        sNode.setTargets(cNode, new RPCServer(sNode, counter));
        
        // We can't set the value as this would trigger old callback and everything nice. One way is to clear callback.
        // But we can just ignore setting value too.
        // cp.setResult(15);
        
        cNode.timeout();
        sNode.deliverAllPackets();
        cNode.deliverAllPackets();

        // This time we should have the result
        assertEquals("Result should NOT be triggered", Status.AtMostOnce, result.status);
        assertEquals("Result should NOT be triggered", null, result.result);
        assertEquals("Result should NOT be triggered", 1, result.invokeCount);
        assertEquals("Function should have been called", 1, counter.invokeCount);
    }

    @Test
    public void testCallPendingException() throws IllegalAccessException, InvocationTargetException
    {        
        client.invoke("callPending", new Serializable[] { 16 }, result);
        sNode.deliverAllPackets();
        cNode.deliverAllPackets();
        
        // set the exception
        cp.setException(new Exception("Pretending that the call failed"));
        cNode.timeout();
        sNode.deliverAllPackets();
        cNode.deliverAllPackets();
        
        // Make sure that the call went in fine
        assertEquals("Server function should have been invoked", 1, counter.invokeCount);
        assertEquals("Callback on client should be invoked", 1, result.invokeCount);
        assertEquals("Server function should have updated its state", 16, counter.counter);
        assertEquals("Server should not have executed function", Status.Exception, result.status);
        assertTrue("Result should be exception", result.result instanceof Exception);
    }
}
